# talib的python代码其实是调用底层的c源码，c源码参考 https://sourceforge.net/p/ta-lib/code/HEAD/tree/trunk/ta-lib/c/src/ta_func/

#获取单只股票的详细数据
import akshare as ak
import pandas as pd
import talib
import numpy as np
import datetime

'''获取A股上市公司分时行情数据'''
def get_aStock_zh_a_minute(symbol, period, adjust):
    '''
    @param: symbol 股票代码； type=str
    @param: period 分钟的数据；比如1, 5, 15, 30, 60； type=str
    @param: adjust 默认为空"": 返回不复权的数据，比如qfq返回前复权后的数据，hfq返回后复权后的数据； type=str
    @return: dataframe
    '''
    # 参数校验
    if (type(symbol) is str and type(period) is str and type(adjust) is str):
        pass
    else:
        raise Exception("symbol、period、adjust类型应该分别为str、str、str")
    stock_zh_a_spot_df = ak.stock_zh_a_minute(symbol=symbol, period=period, adjust=adjust)
    return stock_zh_a_spot_df

'''获取某只A股票的历史（日时）行情数据'''
def get_daily_stock(symbol,start_date,end_date,adjust):
    '''
    @param: symbol 股票代码； type=str
    @param: start_date 起始日期；比如19900101； type=str
    @param: end_date 结束日期；比如21000118； type=str
    @param: adjust 默认为空"": 返回不复权的数据，比如qfq返回前复权后的数据，hfq返回后复权后的数据； type=str
    @return: dataframe
    '''
    # 参数校验
    if (type(symbol) is str and type(start_date) is str and type(end_date) is str and type(adjust) is str):
        pass
    else:
        raise Exception("symbol、start_date、end_date、adjust类型应该分别为str、str、str、str")
    stock_zh_a_daily_qfq_df = ak.stock_zh_a_daily(symbol=symbol, start_date=start_date, end_date=end_date, adjust=adjust)  # 默认返回不复权的数据
    return stock_zh_a_daily_qfq_df

def cal_high_low_volume(stock_segmant):
    '''
    @param stock_segmant 一定期间范围内的股票数据 (至少包括high,low,close和volume字段)； type=dataframe
    @return high,low,volume
    '''
    high,low,volume = np.max(stock_segmant['high']),np.min(stock_segmant['low']),np.sum(get_stock_OBV(stock_segmant))
    return high,low,volume

'''获取某只A股票的周K线数据'''
def get_week_stock(stock_data):
    '''
    @param stock_data 日K线数据; type=dataframe, index是DatetimeIndex
    @return 去掉第一周和最后一周的K线数据（第一周数据是手动去掉的，最后一周的数据是自动去掉的）

    说明：周线就是指一周的K线，以周一开盘价（如果周一不开盘，就从周二开始，以此类推）为周线的开盘价，以周五收盘价为周线收盘价。以一周内的最高价为周线最高价，以一周内的最低价为周线最低价。
    '''
    df_list = []
    # 根据日期获取星期几 参考 https://mp.weixin.qq.com/s?__biz=MzI1NzczMDIwNw==&mid=2247484101&idx=1&sn=b476b0fe45fe23ca8afb15cfbbcefc64&chksm=ea13b135dd643823d3c12d60a0c458453aa96983c5409f8ab15b9799f2343fda696faf12edd8&scene=27
    pre = stock_data.index[0]
    day, open, high, low, close, volume = 0, 0, 0, 0, 0, 0  # 这周开盘日期，开盘价，最高价，最低价，收盘价，成交量（这周能量潮之和）
    for i in range(len(stock_data.index[1:-2])):
        current,next = stock_data.index[i], stock_data.index[i+1]  #获取当前行和下一行的时间索引
        if(pre.dayofweek >= current.dayofweek):  #开盘：周一开盘
            pre = current
            open = stock_data.loc[current]['open']  #开盘价
        elif(current.dayofweek > next.dayofweek): #收盘
            close = stock_data.loc[current]['close']  #收盘价
            #计算最高价，最低价，成交量
            high, low, volume = cal_high_low_volume(stock_data[pre:next])
            temp = [pre, open, high, low, close, volume]
            df_list.append(temp)

    #ndarray转dataframe
    # stock_data.columns = Index(['date', 'open', 'high', 'low', 'close', 'volume', 'outstanding_share','turnover'], dtype='object')
    week_stock_data = pd.DataFrame(df_list, columns=stock_data.columns[0:-2])
    week_stock_data.index = week_stock_data['date']
    return week_stock_data[1:]  #第一周的数据不完整（比如周二开市，周五收市），去掉第一周的数据

'''获取某只A股票的月K线数据'''
def get_month_stock(stock_data):
    '''
    @param stock_data 日K线数据; type=dataframe
    @return 月K线数据（已起始日期为中心，丢弃最后一个月的数据）

    说明：在股市中，一般以一个日期为中心，在这个日期经过一个月后，就会算一个周期，长久下来，几个周期逐渐组成了月线。
    '''
    df_list = []
    # 根据日期获取星期几 参考 https://mp.weixin.qq.com/s?__biz=MzI1NzczMDIwNw==&mid=2247484101&idx=1&sn=b476b0fe45fe23ca8afb15cfbbcefc64&chksm=ea13b135dd643823d3c12d60a0c458453aa96983c5409f8ab15b9799f2343fda696faf12edd8&scene=27
    current = stock_data.index[0]  # pd.TimeStramp类型
    day, open, high, low, close, volume = 0, 0, 0, 0, 0, 0  # 这月开盘日期，开盘价，最高价，最低价，收盘价，成交量（这周能量潮之和）

    days_of_month = current.days_in_month  #当前月的天数
    current_datetime = datetime.datetime.strptime(str(current),'%Y-%m-%d %H:%M:%S')
    next_datetime = current_datetime + datetime.timedelta(days=days_of_month)   #当前日期加一个月
    # 判断下个月的截至日期是否包含在股票数据中（注意next_datetime可能是在周末，因此不能用 (next_datetime in stock_data.index)来判断
    while((next_datetime > stock_data.index).min() == False):

        #可能会报错：{IndexingError}Unalignable boolean Series provided as indexer (index of the boolean Series and of the indexed object do not match).
        #原因是包含True/False的mask序列长度和df长度不一致 参考https://www.cnpython.com/qa/1536639
        # month_stock_data = stock_data[stock_data[stock_data['date'] >= current_datetime]['date'] <= next_datetime]  #获取这个月的股票数据
        #解决方法
        mask = (stock_data['date'] >= current_datetime) & (stock_data['date'] <= next_datetime)
        month_stock_data = stock_data[mask]  #获取这个月的股票数据

        open = month_stock_data.loc[month_stock_data.index[0]]['open']  # 开盘价
        close = month_stock_data.loc[month_stock_data.index[-1]]['close']  # 收盘价
        high,low,volume = cal_high_low_volume(month_stock_data)
        temp = [current_datetime,open,high,low,close,volume]
        df_list.append(temp)

        current_datetime = next_datetime   #current_datetime和next_datetime不一定为交易日
        next_datetime = current_datetime + datetime.timedelta(days=days_of_month)  # 当前日期加一个月

    # ndarray转dataframe
    # stock_data.columns = Index(['date', 'open', 'high', 'low', 'close', 'volume', 'outstanding_share','turnover'], dtype='object')
    months_stock_data = pd.DataFrame(df_list, columns=stock_data.columns[0:-2])
    months_stock_data.index = months_stock_data['date']
    return months_stock_data

'''获取股票的MA指标'''
def get_stock_MA(stock_data,timeperiod,matype):
    '''
     @param stock_data 股票每日的收盘价; type=ndarray
     @param timeperiod 滑动时间窗口长度，默认30天; type=int
     @param matype MA的指标类型; 指标类型matype分别对应：0=SMA, 1=EMA, 2=WMA, 3=DEMA, 4=TEMA, 5=TRIMA, 6=KAMA, 7=MAMA, 8=T3 (Default=SMA)

    参考资料： https://zhuanlan.zhihu.com/p/447716939，https://mp.weixin.qq.com/s/nLOCNim0XRjqs_2iLDMO8Q

    公式说明：https://baike.baidu.com/item/%E7%A7%BB%E5%8A%A8%E5%B9%B3%E5%9D%87%E7%BA%BF/217887?fromtitle=MA&fromid=1511750&fr=aladdin
    - 移动平均线系列指标包括：
        SMA简单移动平均线、
        EMA指数移动平均线、
        WMA加权移动平均线、
        DEMA双移动平均线、
        TEMA三重指数移动平均线、
        TRIMA三角移动平均线、
        KAMA考夫曼自适应移动平均线、
        MAMA为MESA自适应移动平均线、
        T3三重指数移动平均线。
    - 移动平均线（MA）按时间周期长短分为：短期移动平均线，中期移动平均线，长期移动平均线；按计算方法分为算术移动平均线，加权移动平均线，指数平滑移动平均线（EMA）。
    - MA的计算公式：
        - 算术移动平均线 (MA) = N日收盘价之和 / N
        - 线性加权移动平均线 (WMA) = (C1 * 1 + C2 * 2 + ... Cn * n) / (1 + 2 + ... + n)
        - 指数平滑移动平均线（EMA，MACD用到EMA):
            参考 https://www.zhihu.com/question/50889736/answer/2559137500
            EMA(n) = 前一日EMA(n)x(n-1)/(n+1) +今日收盘价x2/(n+1); 其中首次EMA(12)为上一期的收盘价
                    比如EMA(12) = 前一日EMA(12) x 11/13 +今日收盘价x2/13
    - 策略：
        1）上升行情初期，短期移动平均线从下向上突破中长期移动平均线，形成的交叉叫黄金交叉，预示股价将上涨：
            黄色的5日均线上穿紫色的10日均线形成的交叉；10日均线上穿绿色的30日均线形成的交叉均为黄金交叉。
        2）当短期移动平均线向下跌破中长期移动平均线形成的交叉叫做死亡交叉。预示股价将下跌。
            黄色的5日均线下穿紫色的10日均线形成的交叉；10日均线下穿绿色的30日均线形成的交叉均为死亡交叉。
        但是，不是所有的黄金交叉和死亡交叉都是进货点和出货点。原因是庄家有时会进行骗线。尤其是在上升途中或者下跌途中，庄家可能会进行震荡洗盘或震荡出货。此时，黄金交叉和死亡交叉所指示的买卖点是非常不可靠的
    '''
    res = talib.MA(stock_data,timeperiod=timeperiod,matype=matype)
    return res

# '''获取股票的MACD指标'''
def get_stock_MACD(stock_data,fastperiod=12,slowperiod=26,signalperiod=9):
    '''
     @param stock_data 股票每日的收盘价; type=ndarray
     @param fastperiod: 12 短线时间窗口长度
     @param slowperiod: 26 长线时间窗口长度
     @param signalperiod: 9
     @return macd 白线
            macdsignal 黄线
            macdhist 柱状图

    参考 https://zhuanlan.zhihu.com/p/447716939, https://www.zhihu.com/question/50889736/answer/2559137500
    公式说明：
        - MACD指标默认的参数设置是MACD(12、26、9)，12是SHORT值，代表短期均线; 26是LONG值，代表长期均线; 9是MID值，是对长短均线离差值计算平均时的长度。，是代表DEA 9个交易日内DIFF的平均数。这些参数的大小直接影响着MACD指标的变化。
        - 1）差离值：白线DIF = EMA(12)-EMA(26)
          2）讯号线：黄线DEA =前一日DEAx8/10 +今日DIFx2/10，如果把"DIF"入为日"收盘价"，则讯号线就是白线DIF的EMA(9)。
          3）(DIF-DEA)x2即为MACD柱状图，大于零为红柱子，小于零为绿柱子
    '''
    macd,macdsignal,macdhist = talib.MACD(stock_data,fastperiod=fastperiod,slowperiod=slowperiod,signalperiod=signalperiod)
    return macd,macdsignal,macdhist

'''获取股票的RSI指标'''
def get_stock_RSI(stock_data,timeperiod):
    '''
     @param stock_data 股票每日的收盘价; type=ndarray
     @param timeperiod 滑动时间窗口长度，默认30天; type=int

    公式说明：参考 https://zhuanlan.zhihu.com/p/447716939
    RSI公式:  A——N日内收盘涨幅之和
             B——N日内收盘跌幅之和(取正值)
             N日 RSI = A/(A+B)×100
    RSI的变动范围在0—100之间，强弱指标值一般分布在20—80。RSI值市场特征投资操作：
        80—100极强卖出
        50—80强买入
        20—50弱观望
        0—20极弱买入
    '''
    res = talib.RSI(stock_data,timeperiod=timeperiod)
    return res

'''获取股票的布林带指标'''
def get_Stock_Bollinger(stock_data,timeperiod=2,nbdevup=2,nbdevdn=2,matype=0):
    '''
    源码参考 https://sourceforge.net/p/ta-lib/code/HEAD/tree/trunk/ta-lib/c/src/ta_func/ta_BBANDS.c#l155
    @param stock_data 股票每日的收盘价; type=ndarray
    @param timeperiod 滑动时间窗口长度，默认30天; type=int
    @param nbdevup Deviation multiplier for upper band; 上轨带的偏差乘数
    @param nbdevdn Deviation multiplier for lower band; 下轨带的偏差乘数
    @param matype 滑动平均（MA）的指标类型; 指标类型matype分别对应：0=SMA, 1=EMA, 2=WMA, 3=DEMA, 4=TEMA, 5=TRIMA, 6=KAMA, 7=MAMA, 8=T3 (Default=SMA)
    @return 中轨线，上轨线，下轨线

    公式说明：参考 https://zhuanlan.zhihu.com/p/447716939，https://mp.weixin.qq.com/s/nLOCNim0XRjqs_2iLDMO8Q
        - 布林带(Bollinger Bands)由约翰·布林先生创造，其利用统计原理，求出股价的标准差及其信赖区间，从而确定股价的波动范围及未来走势，利用波带显示股价的安全高低价位。其上下限范围不固定，随股价的滚动而变化。
          布林指标属于路径指标，股价波动在上限和下限的区间之内，这条带状区的宽窄，随着股价波动幅度的大小而变化，股价涨跌幅度加大时，带状区变宽，涨跌幅度狭小盘整时，带状区则变窄。
            中轨线 = N日的移动平均线
            上轨线 = 中轨线 + K倍的标准差
            下轨线 =中轨线 － K倍的标准差（K为参数，一般默认为2）
        - 股价高于这个波动区间，即突破阻力线，说明股价虚高，卖出信号；股价低于这个波动区间，即跌破支撑线，说明股价虚低，买入信号
        - 个人感觉：上轨线和下轨线差值大则股票振荡大；上轨线和下轨线同时上升为股价升，上轨线和下轨线同时下降为股价降
    '''
    H_line, M_line, L_line = talib.BBANDS(stock_data, timeperiod=timeperiod, nbdevup=nbdevup, nbdevdn=nbdevdn, matype=matype)
    return H_line,M_line,L_line

'''获取股票的obv能量潮指标'''
def get_stock_OBV(stock_data):
    '''
    @param stock_data 股票数据（包括"收盘价"和"成交量"）
    @return obv能量潮

    能量潮（On Balance Volume，OBV）指标是葛兰碧于20世纪60年代提出的，并被广泛使用。股市技术分析的四大要素：价、量、时、空。OBV指标就是从“量”这个要素作为突破口，来发现热门股票、分析股价运动趋势的一种技术指标。
    公式说明：
        - 今日OBV = 前天的OBV值 ± 交易量，比如参考 https://zhuanlan.zhihu.com/p/436922772
                第一天：收盘价为50.00美元，交易量为10 000股
                第二天：收盘价为50.10美元，交易量为12500股
                第三天：收盘价为50.15美元，交易量为11000股
                第四天：收盘价为50.14美元，交易量为14000股
                第五天：收盘价为50.14美元，交易量为12000股
            从上面的例子中，我们可以看到，第二和第三天的价格是上涨的日子，而其余的是下跌的日子。上涨日的成交量被加入到OBV中，而下跌日的成交量被减去。以下是根据上述数据，每天的OBV是如何计算的。
                第一天的OBV = 0
                第二天的OBV = 0 + 12500 = 12500
                第三天的OBV = 12500 + 11000 = 23500
                第四天OBV = 23500 – 14000 = 9500
                第五天的OBV = 9500
        - 由于OBV的计算方法过于简单化，所以容易受到偶然因素的影响，为了提高OBV的准确性，可以采取多空比率净额法对其进行修正。多空比率净额= [（收盘价－最低价）－（最高价-收盘价）] ÷（ 最高价－最低价）× V
        - 能量潮理论成立的依据主要是：
            1、投资者对股价的评论越不一致，成交量越大；反之，成交量就小。因此，可用成交量来判断市场的人气和多空双方的力量。
            2、重力原理，上升的物体迟早会下跌，而物体上升所需的能量比下跌时多。涉及到股市则可解释为：一方面股价迟早会下跌；另一方面，股价上升时所需的能量大，因此股价的上升特别是上升初期必须有较大的成交量相配合；股价下跌时则不必耗费很大的能量，因此成交量不一定放大，甚至有萎缩趋势。
            3、惯性原则——动则恒动、静则恒静，只有那些被投资者或主力相中的热门股会在很大一段时间内成交量和股价的波动都比较大，而无人问津的冷门股，则会在一段时间内，成交量和股价波幅都比较小。
        - 应用法则
            1、当股价上升而OBV线下降，表示买盘无力，股价可能会回跌。
            2、股价下降时而OBV线上升，表示买盘旺盛，逢低接手强股，股价可能会止跌回升。
            3、OBV线缓慢上升，表示买气逐渐加强，为买进信号。
            4、OBV线急速上升时，表示力量将用尽为卖出信号。
            5、OBV线最大的用处，在于观察股市盘局整理后，何时会脱离盘局以及突破后的未来走势，OBV线变动方向是重要参考指数，其具体的数值并无实际意义。
        - 缺点：
            - OBV指标是建立在国外成熟市场上的经验总结。把它移植到国内必然要经过一番改造才行。比如价涨量增，用在内地股市坐庄的股票上就不灵了，这时股价涨得越高成交量反而越少。这是因为主力控盘较重，股价在上涨过程中没有获利筹码加以兑现，所以此时股票会涨得很“疯”，但成交量并不增加，OBV自然就无法发挥作用。
            - 另外，涨跌停板的股票也会导致指标失真。由于内地股市采用了涨跌停板的限制，很多股票在连续涨停的时候，由于股民预期后市会继续大涨，往往会持股观望，导致出现越涨越无量的现象。因此，对于那些达到涨跌停板的股票，OBV指标也无法正常发挥作用。
            - 目前，内地股市仍有大量的“坐庄”现象存在。对于中长线庄家而言，需要在股价处于底部的时候尽可能地吸进大量筹码，然后拉到高处派发。在底部收集筹码阶段，必然会由于庄家的买进使股价有一定上涨，同时伴随成交量放大。这时候，为了降低吸筹成本，庄家会把小幅上涨的股价向下打压，到底部后继续吸筹。如此反复，直到吸到足够的筹码为止。
    '''

    obvta = talib.OBV(stock_data['close'], stock_data['volume'])
    # obv = []
    # for i in range(0, len(stock_data)):
    #     if i == 0:
    #         obv.append(stock_data['volume'].values[i])
    #     else:
    #         if stock_data['close'].values[i] > stock_data['close'].values[i - 1]:
    #             obv.append(obv[-1] + stock_data['volume'].values[i])
    #         if stock_data['close'].values[i] < stock_data['close'].values[i - 1]:
    #             obv.append(obv[-1] - stock_data['volume'].values[i])
    #         if stock_data['close'].values[i] == stock_data['close'].values[i - 1]:
    #             obv.append(obv[-1])
    # df['obv'] = obv
    # return obv
    return obvta

